/*
 * !Drowning
 * May 29, 2011
 * TCSS 360
 * Schedule Project
 */
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


/**
 * Contain all logic pertaining to schedule and the ability to check 
 * constraints aswell.
 * 
 * @author David Phillips
 * @version 6/1/2011
 */
public class Schedule
{
	/**
	 * The maximum amount of credit a instructor can have in a single 
	 * schedule.
	 */
	private int my_max_credits = 15;
	
	/**
	 * The defined cutoff time between day and night;
	 */
	private int my_day_night_time = 1615; //4:15p
	
	/**
	 * The map that contains all the instructors and their information 
	 * relating to the courses they teach.
	 */
	private Map<String, InstructorInfo> my_instructors;
	
	/**
	 * Holds all the information to be saved regarding instructors that 
	 * are going over the maximum credit limit.
	 */
	private List<String> instructorOverCredits;
	
	/**
	 * Holds all the information to be saved regarding to the instructors 
	 * that have time conflicts within the schedule.
	 */
	private List<String> instructorConflictingTimes;
	
	/**
	 * Holds all the information to be saved regarding suggestions to course 
	 * section spacing regarding day and time differences.
	 */
	private List<String> courseSectionSpacing;
	
	/**
	 * Holds all the information to be saved regarding students and 
	 * their suggest classes to be put on the schedule.
	 */
	private List<String> studentPreferences;
	
	/**
	 * Holds all the information to be saved regarding instructor and 
	 * their suggest classes to be put on the schedule.
	 */
	private List<String> instructorPreferences;
	
	/**
	 * Holds all the information to be saved regarding advisor and 
	 * their suggest classes to be put on the schedule.
	 */
	private List<String> advisorPreferences;
	
	/**
	 * The amount of preferences to show.
	 */
	private int amountPrefToShow = 10;
	
	/**
	 * Default Constructor
	 */
	public Schedule(){
		
	}
	
	/**
	 * Sets the maximum credit limit a teacher is allowed to have.
	 * (Default 15)
	 * @param a_limit the new limit.
	 */
	public void setCreditLimit(int a_limit){
		my_max_credits = a_limit;
	}
	
	/**
	 * Sets the amount of preferences to show.
	 * (Default 10)
	 * @param a_limit the new limit.
	 */
	public void setAmountPrefToShow(int a_limit){
		amountPrefToShow = a_limit;
	}
	
	/**
	 * Runs all the tests to test constraints.
	 * @param the_courses the course in the current schedule.
	 * @param the_users the use being consider with regards to the schedule.
	 * @return All the constraints in a  text format.
	 */
	@SuppressWarnings("unchecked")
	public String doTests(Courses the_courses, Users the_users){
		
		//DEBUG
		System.out.println("\nSchedule ----- Constraints Checker\n\tPrecheck");
		System.out.println("\tCourses size: "+the_courses.getCourses().size());
		System.out.println("\tUsers size: "+ (the_users.getStudents().size() + 
				the_users.getAdvisors().size() + the_users.getInstructors().size()));
		
		String the_constraints_not_met = null;
		my_instructors = new HashMap<String, InstructorInfo>();
		courseSectionSpacing = new ArrayList<String>();
		instructorOverCredits = new ArrayList<String>();
		instructorConflictingTimes = new ArrayList<String>();
		studentPreferences = new ArrayList<String>();
		instructorPreferences = new ArrayList<String>();
		advisorPreferences = new ArrayList<String>();
		
		constraints(the_courses);
		userPreferences(the_courses, the_users);
		
		//build return string
		the_constraints_not_met = "Schedule Constraints Checker\n";
		
		the_constraints_not_met = the_constraints_not_met + "\nADVISORS\n";
		the_constraints_not_met = the_constraints_not_met + addAllStrings(advisorPreferences, 
				"Advisors preferred courses to add [Top " + amountPrefToShow + "]");
		
		the_constraints_not_met = the_constraints_not_met + "\nINSTRUCTORS\n";
		the_constraints_not_met = the_constraints_not_met + addAllStrings(instructorOverCredits, 
				"Instructor over credit limit (" + my_max_credits + ")");
		the_constraints_not_met = the_constraints_not_met + "\n" + addAllStrings(instructorConflictingTimes, 
				"Instructor Schedule Conflicts");
		the_constraints_not_met = the_constraints_not_met + "\n" + addAllStrings(instructorPreferences, 
		"Instructors preferred courses to add [Top " + amountPrefToShow + "]");
		
		the_constraints_not_met = the_constraints_not_met + "\nSTUDENTS\n";
		the_constraints_not_met = the_constraints_not_met + addAllStrings(studentPreferences, 
				"Students preferred courses to add [Top " + amountPrefToShow + "]");
		
		the_constraints_not_met = the_constraints_not_met + "\nSUGGESTIONS\n";
		the_constraints_not_met = the_constraints_not_met + addAllStrings(courseSectionSpacing, "Course Spacing");	
		
		
		return the_constraints_not_met;
	}
	
	private String addAllStrings(List<String> the_list, String Title){
		String the_return_string = Title + " ";
		if(Title.length() < 99){
			for(int i = 0; i < 100 - Title.length(); i++){
				the_return_string = the_return_string + "=";
			}
		}
		the_return_string = the_return_string + "\n";
		for(int j = 0; j < the_list.size(); j++){
			the_return_string = the_return_string + "\t" + the_list.get(j) + "\n";
		}
		return the_return_string;
	}
	
	
	/**
	 * Computes all the user preferences.
	 * @param the_courses the course being considered.
	 * @param the_users the users being considered.
	 */
	private void userPreferences(Courses the_courses, Users the_users) {
		Map<String, Course> the_offered_courses = buildOfferedCourses(the_courses);
		
		studentPreferences = checkOfferings(the_offered_courses, the_users.getStudents(), "students");
		instructorPreferences = checkOfferings(the_offered_courses, the_users.getInstructors(), "instructors");
		advisorPreferences = checkOfferings(the_offered_courses, the_users.getAdvisors(), "advisors");
	}

	/**
	 * Populates a map with current course and then populates another map 
	 * with course that users want to be offered at specific times.
	 * @param the_offered_courses the current course being offered at certain times.
	 * @param current_group the current group of users being checked.
	 * @param a_label the label of the groups used for output.
	 * @return the sting output of all the suggested classes.
	 */
	private List<String> checkOfferings(Map<String, Course> the_offered_courses,
			List<User> current_group, String a_label) {
		Map<String, Integer> wanted_map = new HashMap<String, Integer>();
		List<String> wanted_classes = new ArrayList<String>();
		for(int i = 0; i < current_group.size(); i++){
			User a = current_group.get(i);
			for(preference b: a.getPreferences()){
				boolean preferenceMatch = false;
				if(the_offered_courses.containsKey(b.getCourseId())){
					for(@SuppressWarnings("rawtypes") List c: the_offered_courses.get(b.getCourseId()).getSections()){
						if(c.get(2) == b.getDay() && 
								(Integer)c.get(3) == b.getStartTime() && 
								(Integer)c.get(4) == b.getEndTime()){
							preferenceMatch = true;
						}
					}
				}
				if(!preferenceMatch){
					String key = b.getCourseId() + " on " + b.getDay() + " starting at " + 
							b.getStartTime() + " to " + b.getEndTime();
					if(wanted_map.containsKey(key)){
						wanted_map.put(key, wanted_map.get(key)+ 1);
					}
					else{
						wanted_map.put(key, 1);
					}
				}
			}
		}
		
		//DEBUG
		System.out.println("\n\t" + a_label + " preference matching");
		wanted_map = sortByValue(wanted_map);
		Iterator<Entry<String, Integer>> it = wanted_map.entrySet().iterator();
		int counter = 0;
		while (it.hasNext() && counter != amountPrefToShow) {
			Entry<String, Integer> pair = it.next();
			wanted_classes.add(pair.getValue() + " " + a_label + " would like to have " + pair.getKey());
			
			//DEBUG
			System.out.println("\t" + pair.getValue() + " " + a_label + " would like to have " + pair.getKey());
			counter++;
		}
		
		return wanted_classes;
	}

	
	/**
	 * Iterates through all course and their section calling on 
	 * constraints to check for issues and violations of constraints.
	 * @param the_courses the courses being checked.
	 */
	private void constraints(Courses the_courses){		
		ArrayList<Course> all_courses = (ArrayList<Course>) the_courses.getCourses();
		
		//DEBUG
		System.out.println("\n\tCourse Spacing Suggestions\n");
		
		//For all courses
		for(int i = 0; i < all_courses.size(); i++){
			String[] current_sections = all_courses.get(i).getSectionNames();
			int course_size = current_sections.length;
			
			if(course_size > 1){
				checkSectionSpacing(all_courses.get(i));
			}
			
			//For all sections of course
			for(int j = 0; j < course_size; j++){
				Course current_course = all_courses.get(i);
				
				// For Instructors
				buildInstructorMap(current_sections[j], current_course);	
			}		
		}
		checkInstructorConstraints();	
	}
	
	
	/**
	 * Iterates through the list of instructor's information 
	 * looking for constraints regarding instructors.
	 */
	private void checkInstructorConstraints(){

		//DEBUG
		System.out.println("\n\tInstructor Credit Hour Overages\n\t\t&\n\tschedule conflicts\n");
		
		Iterator<Entry<String, InstructorInfo>> it = my_instructors.entrySet().iterator();
	    while (it.hasNext()) {
	        Map.Entry<String, InstructorInfo> current_instructor = it.next();        
	        checkInstructorCredits(current_instructor);
	        checkInstructorTimeConflicts(current_instructor);
		}
	}
	
	/**
	 * Sorts the Map map by value, descending.
	 * @author Unknown http://www.programmersheaven.com/download/49349/download.aspx
	 * @author David Phillips; Added the ability to sort descending.
	 * @version 6/1/2011
	 * 
	 * @param map, the map being sorted.
	 * @return
	 */
	private Map sortByValue(Map map) {
	     List list = new LinkedList(map.entrySet());
	     Collections.sort(list, new Comparator() {
	          public int compare(Object o1, Object o2) {
	               return ((Comparable) ((Map.Entry) (o2)).getValue())
	              .compareTo(((Map.Entry) (o1)).getValue());
	          }
	     });

	    Map result = new LinkedHashMap();
	    for (Iterator it = list.iterator(); it.hasNext();) {
	        Map.Entry entry = (Map.Entry)it.next();
	        result.put(entry.getKey(), entry.getValue());
	    }
	    return result;
	} 
	
	/**
	 * Gather all the offered courses into a map.
	 * @param the_courses the course being added.
	 * @return a map filled with course from the_courses.
	 */
	private Map<String, Course> buildOfferedCourses(Courses the_courses) {
		Map<String, Course> the_offered_courses = new HashMap<String, Course>();
		for(Course a: the_courses.getCourses()){
			the_offered_courses.put(a.getId(), a);
		}
		return the_offered_courses;
	}
	
	/**
	 * Checks all course and the sections to make sure that are varied by 
	 * date in time to allow for optimum availability to instructors and users.
	 * @param the_course the course being checked.
	 */
	private void checkSectionSpacing(Course the_course){
		boolean allSameDay = true;
		boolean allSameDayTime = true;
		String[] sections = the_course.getSectionNames();
		for(int i=1; i < sections.length; i++){
			if(!the_course.getDays(sections[i-1]).equals(the_course.getDays(sections[i]))){
				allSameDay = false;
			}
			if((the_course.getStartTime(sections[i-1]) - my_day_night_time > 0 && 
					the_course.getStartTime(sections[i]) - my_day_night_time < 0) ||
					(the_course.getStartTime(sections[i]) - my_day_night_time > 0 && 
					the_course.getStartTime(sections[i-1]) - my_day_night_time < 0)){
				allSameDayTime = false;
			}
		}
		
		if(allSameDay || allSameDayTime){
			String savedString = "Course " + the_course.getId() + "\thas multiple sections on the same ";
			if(allSameDay && allSameDayTime){
				savedString = savedString + "day and time of the day";
			}
			else{
				if(allSameDay){
					savedString = savedString + "same day";
				}
				else{
					savedString = savedString + "time of the day";
				}
			}
			//DEBUG
			System.out.println("\t"+savedString);
			
			courseSectionSpacing.add(savedString);
		}	
	}
	
	
	/**
	 * Add to the map with a new instructor information or a new instructor.
	 * @param the_current_section the current section
	 * @param current_course the current course
	 */
	private void buildInstructorMap(String the_current_section, Course current_course){
		String current_instructor = current_course.getInstructor(the_current_section);
		InstructorInfo currentInstructor = null;
		
		if(!my_instructors.containsKey(current_instructor)){
			my_instructors.put(current_instructor, new InstructorInfo(current_instructor));
		}
		currentInstructor =  my_instructors.get(current_instructor);
		
		// add credit load to instructor
		currentInstructor.addCredits(current_course.getCredits());
		
		//add times to instructor
		currentInstructor.addTime(current_course.getStartTime(the_current_section), 
				current_course.getEndTime(the_current_section),
				"" + current_course.getId() + the_current_section,
				current_course.getDays(the_current_section));
		
		my_instructors.put(currentInstructor.getName(), currentInstructor);
	}
	
	/**
	 * Checks to see if instructor has violated the maximum credit load for an instructor.
	 * @param current_instructor the instructor currently being looked at.
	 */
	private void checkInstructorCredits(Map.Entry<String, InstructorInfo> current_instructor){
		//Checks for credit overage
        if(current_instructor.getValue().getCreditLoad() > my_max_credits){
        	instructorOverCredits.add(current_instructor.getValue().getName() + 
        		" is scheduled for " + current_instructor.getValue().getCreditLoad() + 
        		"/" + my_max_credits +" credits");
        	
        	//DEBUG
        	System.out.println("\t"+current_instructor.getValue().getName() + 
	        		" is scheduled for " + current_instructor.getValue().getCreditLoad() + 
	        		"/" + my_max_credits +" credits");
        }
	}
	
	/**
	 * Check instructors schedule and looks for time conflicts for that particular instructor.
	 * @param current_instructor the current instructor being looked at.
	 */
	private void checkInstructorTimeConflicts(Map.Entry<String, InstructorInfo> current_instructor){
		List<int[]> the_times = current_instructor.getValue().getTimes();
        List<String[]> the_classes = current_instructor.getValue().getClasses();
        for(int i = 0; i < the_times.size(); i++){
        	for(int j = i; j < the_times.size(); j++){
        		if(i != j){
        			if(((the_times.get(j).equals(the_times.get(i))) ||
        			   (the_times.get(j)[0] >= the_times.get(i)[0] && the_times.get(j)[0] <= the_times.get(i)[1]) ||
        			   (the_times.get(i)[0] >= the_times.get(j)[0] && the_times.get(i)[0] <= the_times.get(j)[1])) &&
        			    the_classes.get(i)[1].equals(the_classes.get(j)[1])){				     
    					instructorConflictingTimes.add(current_instructor.getValue().getName() +
    					" has a time conflict while teaching " + the_classes.get(i)[0] + " and " + the_classes.get(j)[0]);
    					
    					//DEBUG
    					System.out.println("\t"+current_instructor.getValue().getName() +
	        					" has a time conflict while teaching " + the_classes.get(i)[0] + " and " + the_classes.get(j)[0]);
        			}
        		}
        	}
        }
	}
	
	/**
	 * Holds schedule specific information for the instructor 
	 * during testing within schedule.
	 * @author David Phillips
	 * @version 6/1/2011
	 */
	 private class InstructorInfo {
		 	/**
		 	 * the current credit load of the intructor.
		 	 */
	        private int creditLoad = 0;
	        
	        /**
	         * A list of all the times, start and end of 
	         */
	        private List<int[]> classTimes = new ArrayList<int[]>();
	        
	        /**
	         * The ids of all the course the instructor is teaching.
	         */
	        private List<String[]> coursesTaught = new ArrayList<String[]>();
	        
	        /**
	         * The name of the instructor.
	         */
	        private String name;
	        
	        /**
	         * The default constructor setting the name aswell.
	         * @param a_name the instrcutors name.
	         */
	        public InstructorInfo(String a_name){
	        	name = a_name;
	        }
	        
	        /**
	         * gets the name of the instrcutor.
	         * @return instrcutors name.
	         */
	        public String getName(){
	        	return name;
	        }
	        
	        /**
	         * Add credits to the instructor.
	         * @param the_credits the credits being added.
	         */
	        public void addCredits(int the_credits){
	        	creditLoad += the_credits;
	        }
	        
	        /**
	         * Returns the current credit load of the instructor.
	         * @return the credit load.
	         */
	        public int getCreditLoad(){
	        	return creditLoad;
	        }
	        
	        /**
	         * Add a new class that the instrcutor teaches.
	         * @param the_start the start time of the class.
	         * @param the_end the end time of the class.
	         * @param the_id the course id.
	         * @param the_day the day(s) of the course.
	         */
	        public void addTime(int the_start, int the_end, String the_id, String the_day){
	        	int[] temp_int_array = new int[2];
	        	temp_int_array[0] = the_start;
	        	temp_int_array[1] = the_end;
	        	classTimes.add(temp_int_array);
	        	String[] temp_string = new String[2];
	        	temp_string[0] = the_id;
	        	temp_string[1] = the_day;
	        	coursesTaught.add(temp_string);
	        }
	        
	        /**
	         * get the complete list of class times.
	         * @return the class times.
	         */
	        public List<int[]> getTimes(){
	        	return classTimes;
	        }
	        
	        /**
	         * get the complete list of all classes taught.
	         * @return the curse taught.
	         */
	        public List<String[]> getClasses(){
	        	return coursesTaught;
	        }
	    }
	 
	 
    /*public void generate(Collection<?> the_items)
    {
        FileOutputStream aFileOutStream;
        PrintStream aPrintStream; 
        try
        { 
                aFileOutStream = new FileOutputStream("constraints.txt");
                aPrintStream = new PrintStream( aFileOutStream );
                aPrintStream.append("This file contains all the constraints " +
                		"that were violated:\n\n");
                Iterator<?> items_itr = the_items.iterator();
                while(items_itr.hasNext())
                {
                    aPrintStream.append((CharSequence) items_itr.next());
                    aPrintStream.append("\n");
                }
                aPrintStream.close();
        }
        catch (Exception e)
        {
                System.err.println ("Error writing to file");
        }
    }*/
}
